﻿using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Imaging;
using Silverlight.Media.Encoder.Sinks;

namespace Silverlight.Media.Encoder
{
    [TemplatePart(Name = PART_VideoImage, Type = typeof(Image))]
    [TemplateVisualState(Name = VSMSTATE_Encoding, GroupName = VSMGROUP_Common)]
    [TemplateVisualState(Name = VSMSTATE_EncodingPrepared, GroupName = VSMGROUP_Common)]
    [TemplateVisualState(Name = VSMSTATE_Normal, GroupName = VSMGROUP_Common)]
    public class EncoderElement : Control
    {
        private const string PART_VideoImage = "PART_VideoImage";
        private const string VSMSTATE_Encoding = "Encoding";
        private const string VSMSTATE_EncodingPrepared = "EncodingPrepared";
        private const string VSMSTATE_Normal = "Normal";
        private const string VSMGROUP_Common = "Common";
       
        private WindowsMediaEncoder m_encoder;
        private AudioVideoCaptureHelper m_avCaptureHelper;

        private Image m_partVideoImage;
        private WriteableBitmap m_videoImage;
        private bool m_ignoreDependencyPropertyChanged = false;
        private bool m_audioInitialized;
        private bool m_videoInitialized;

        private bool m_queueEncoderStart;

        private long m_audioSampleTimeBase;
        private long m_videoSampleTimeBase;

        #region IsEncoding

        public static readonly DependencyProperty IsEncodingProperty =
                DependencyProperty.Register("IsEncoding", 
                                            typeof(bool), 
                                            typeof(EncoderElement),
                                            new PropertyMetadata(false, OnIsEncodingChanged));

        public bool IsEncoding
        {
            get { return (bool)GetValue(IsEncodingProperty); }
            set { SetValue(IsEncodingProperty, value); }
        }

        private static void OnIsEncodingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((EncoderElement)d).OnIsEncodingChanged(e);
        }

        protected virtual void OnIsEncodingChanged(DependencyPropertyChangedEventArgs e)
        {
            if (m_ignoreDependencyPropertyChanged)
            {
                m_ignoreDependencyPropertyChanged = false;
                return;                
            }

            var encoding = (bool)e.NewValue;

            if(encoding)
                StartEncoding();
            else
                StopEncoding();
        }

        private void SetIsEncodingInternal(bool encoding)
        {
            if(IsEncoding != encoding)
                m_ignoreDependencyPropertyChanged = true;

            IsEncoding = encoding;
        }

        #endregion

        #region EncodeFps

        public static readonly DependencyProperty EncodeFpsProperty =
            DependencyProperty.Register("EncodeFps", typeof(int), typeof(EncoderElement),
                new PropertyMetadata(30,OnEncodeFpsChanged));

        public int EncodeFps
        {
            get { return (int)GetValue(EncodeFpsProperty); }
            set { SetValue(EncodeFpsProperty, value); }
        }

        private static void OnEncodeFpsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((EncoderElement)d).OnEncodeFpsChanged(e);
        }

        protected virtual void OnEncodeFpsChanged(DependencyPropertyChangedEventArgs e)
        {
            var fps = (int)e.NewValue;
        }

        #endregion

        #region OutputFilename
        public static readonly DependencyProperty OutputFilenameProperty =
            DependencyProperty.Register("OutputFilename", typeof(string), typeof(EncoderElement),
                new PropertyMetadata(string.Empty, OnOutputFilenameChanged));

        public string OutputFilename
        {
            get { return (string)GetValue(OutputFilenameProperty); }
            set { SetValue(OutputFilenameProperty, value); }
        }

        private static void OnOutputFilenameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((EncoderElement)d).OnOutputFilenameChanged(e);
        }

        protected virtual void OnOutputFilenameChanged(DependencyPropertyChangedEventArgs e)
        {
            SetEncoderProperties();
        }

        #endregion

        #region NetworkStream

        public static readonly DependencyProperty NetworkStreamProperty =
            DependencyProperty.Register("NetworkStream", typeof(bool), typeof(EncoderElement),
                new PropertyMetadata(false, OnNetworkStreamChanged));

        public bool NetworkStream
        {
            get { return (bool)GetValue(NetworkStreamProperty); }
            set { SetValue(NetworkStreamProperty, value); }
        }

        private static void OnNetworkStreamChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((EncoderElement)d).OnNetworkStreamChanged(e);
        }

        protected virtual void OnNetworkStreamChanged(DependencyPropertyChangedEventArgs e)
        {
            SetEncoderProperties();
        }

        #endregion

        #region TcpPort

        public static readonly DependencyProperty TcpPortProperty =
            DependencyProperty.Register("TcpPort", typeof (int), typeof (EncoderElement), new PropertyMetadata(8089 ,OnTcpPortChanged));

        public int TcpPort
        {
            get { return (int)GetValue(TcpPortProperty); }
            set { SetValue(TcpPortProperty, value); }
        }

        private static void OnTcpPortChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((EncoderElement)d).OnTcpPortChanged(e);
        }

        protected virtual void OnTcpPortChanged(DependencyPropertyChangedEventArgs e)
        {
            SetEncoderProperties();
        }

        #endregion
        
        #region EncoderProfileData

        public static readonly DependencyProperty EncoderProfileDataProperty =
            DependencyProperty.Register("EncoderProfileData", typeof(string), typeof(EncoderElement),
                new PropertyMetadata(string.Empty, OnEncoderProfileDataChanged));

        public string EncoderProfileData
        {
            get { return (string)GetValue(EncoderProfileDataProperty); }
            set { SetValue(EncoderProfileDataProperty, value); }
        }

        private static void OnEncoderProfileDataChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((EncoderElement)d).OnEncoderProfileDataChanged(e);
        }

        protected virtual void OnEncoderProfileDataChanged(DependencyPropertyChangedEventArgs e)
        {
            SetEncoderProperties();
        }

        #endregion

        #region InstallationPath

        public static readonly DependencyProperty InstallationPathProperty =
            DependencyProperty.Register("InstallationPath", typeof(string), typeof(EncoderElement),
                new PropertyMetadata(string.Empty,OnInstallationPathChanged));

        public string InstallationPath
        {
            get { return (string)GetValue(InstallationPathProperty); }
            set { SetValue(InstallationPathProperty, value); }
        }

        private static void OnInstallationPathChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((EncoderElement)d).OnInstallationPathChanged(e);
        }

        protected virtual void OnInstallationPathChanged(DependencyPropertyChangedEventArgs e)
        {
        }

        #endregion

        public EncoderElement()
        {
            DefaultStyleKey = typeof(EncoderElement);

            m_avCaptureHelper = new AudioVideoCaptureHelper();
            m_avCaptureHelper.OnMediaSample += OnMediaSampleCallback;
            m_avCaptureHelper.AudioFormatChanged += AudioFormatChangedCallback;
            m_avCaptureHelper.VideoFormatChanged += VideoFormatChangedCallback;

            Loaded += EncoderElement_Loaded;
        }

        private void EncoderElement_Loaded(object sender, RoutedEventArgs e)
        {
            CreateEncoder();
        }

        public override void OnApplyTemplate()
        {
            m_partVideoImage = GetTemplateChild(PART_VideoImage) as Image;

            base.OnApplyTemplate();
        }

        private void VideoFormatChangedCallback(object sender, VideoFormatChangedArgs e)
        {
            int width = 0;
            int height = 0;

#if SL4BETA
            width = e.Format.Width;
            height = e.Format.Height;
#else
            width = e.Format.PixelWidth;
            height = e.Format.PixelHeight;
#endif
            Dispatcher.BeginInvoke(delegate
            {
                m_videoImage = new WriteableBitmap(width, height);
                m_partVideoImage.Source = m_videoImage;
            });
           
            m_encoder.InitializeVideo(width, height, 32);
            m_videoInitialized = true;
        }

        private void AudioFormatChangedCallback(object sender, AudioFormatChangedArgs e)
        {
            m_encoder.InitializeAudio((short)e.Format.Channels, (short)e.Format.BitsPerSample, e.Format.SamplesPerSecond);
            m_audioInitialized = true;
        }

        private void OnMediaSampleCallback(object sender, OnSampleArgs e)
        {
            if (!Dispatcher.CheckAccess())
            {
                Dispatcher.BeginInvoke(() => OnMediaSampleCallback(sender, e));
                return;
            }
            
            if (m_queueEncoderStart)
            {
                bool startForSure = false;

                if(m_avCaptureHelper.HasVideoSource && m_avCaptureHelper.HasAudioSource)
                {
                    if(m_audioInitialized && m_videoInitialized)
                    {
                        startForSure = true;
                    }
                }
                else if (m_avCaptureHelper.HasVideoSource && !m_avCaptureHelper.HasAudioSource)
                {
                    if (m_videoInitialized)
                    {
                        /* Hack to make encoder not crash.  It needs both audio and video initied */
                        m_encoder.InitializeAudio(2, 16, 44100);
                        m_audioInitialized = true;
                        startForSure = true;
                    }
                }
                else if (!m_avCaptureHelper.HasVideoSource && m_avCaptureHelper.HasAudioSource)
                {
                    if (m_audioInitialized)
                    {
                        startForSure = true;
                    }
                }

                if (startForSure)
                {
                    m_queueEncoderStart = false;
                    StartInternal();
                }
            }

            switch (e.SampleDataType)
            {
                case SampleType.Video:
                    
                    Buffer.BlockCopy(e.SampleData, 0, m_videoImage.Pixels, 0, e.SampleData.Length);
                    m_videoImage.Invalidate();
                    if (m_encoder != null)
                        if (m_encoder.IsStarted)
                        {
                            if (m_videoSampleTimeBase == 0)
                                m_videoSampleTimeBase = e.SampleTime.Ticks;

                            m_encoder.AppendVideoSample(e.SampleData, (e.SampleTime.Ticks - m_videoSampleTimeBase));
                        }

                    break;
                case SampleType.Audio:
                    if (m_encoder != null)
                        if (m_encoder.IsStarted)
                        {
                            if (m_audioSampleTimeBase == 0)
                                m_audioSampleTimeBase = e.SampleTime.Ticks;

                            m_encoder.AppendAudioSample(e.SampleData, (e.SampleTime.Ticks - m_audioSampleTimeBase));
                        }
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        private void CreateEncoder()
        {
            m_encoder = new WindowsMediaEncoder(InstallationPath);
        }

        private void SetEncoderProperties()
        {
            if (m_encoder == null)
                return;

            m_encoder.OutputFilename = OutputFilename;
            m_encoder.EncodingProfileData = EncoderProfileData;
            m_encoder.NetworkStream = NetworkStream;
            m_encoder.TcpPort = TcpPort;
        }

        public event EventHandler EncoderPrepared;

        public event EventHandler EncodingStarted;

        public event EventHandler EncodingStopped;

        public void InvokeEncodingStopped()
        {
            EventArgs e = new EventArgs();
            EventHandler handler = EncodingStopped;
            if (handler != null) handler(this, e);
        }

        public void InvokeEncodingStarted()
        {
            var e = new EventArgs();
            EventHandler handler = EncodingStarted;
            if (handler != null) handler(this, e);
        }

        public void InvokeEncoderPrepared()
        {
            var e = new EventArgs();
            EventHandler handler = EncoderPrepared;
            if (handler != null) handler(this, e);
        }

        public bool PrepareEncoding()
        {
            if (IsEncoding)
                return false;

            try
            {
                m_avCaptureHelper.StartCapture();
            }
            catch (Exception)
            {
                return false;
            }

            InvokeEncoderPrepared();
            return true;
        }

        public event EventHandler StartEncodeFailed;

        public void InvokeStartEncodeFailed()
        {
            var e = new EventArgs();
            EventHandler handler = StartEncodeFailed;
            if (handler != null) handler(this, e);
        }

        private void StartInternal()
        {
            m_videoSampleTimeBase = 0;
            m_audioSampleTimeBase = 0;

            try
            {
                m_encoder.Start();
            }
            catch (Exception)
            {
                InvokeStartEncodeFailed();
                return;
            }

            InvokeEncodingStarted();

            SetIsEncodingInternal(true);

            VisualStateManager.GoToState(this, VSMSTATE_Encoding, true);
        }

        public void StartEncoding()
        {
            StopEncoding();

            if (string.IsNullOrEmpty(EncoderProfileData))
                return;

            if (!PrepareEncoding())
                return;

            SetEncoderProperties();

            m_queueEncoderStart = true;
        }

        public void StopEncoding()
        {
            if (m_encoder != null)
            {
                bool wasEncoding = m_encoder.IsStarted;

                m_encoder.Stop();

                SetIsEncodingInternal(false);
                
                if (wasEncoding)
                    InvokeEncodingStopped();
            }

            VisualStateManager.GoToState(this, VSMSTATE_Normal, true);
        }
    }
}
